/*!
 * # Fomantic-UI - Calendar
 * http://github.com/fomantic/Fomantic-UI/
 *
 *
 * Released under the MIT license
 * http://opensource.org/licenses/MIT
 *
 */

;(function ($, window, document, undefined) {

'use strict';

$.isFunction = $.isFunction || function(obj) {
  return typeof obj === "function" && typeof obj.nodeType !== "number";
};

window = (typeof window != 'undefined' && window.Math == Math)
  ? window
  : (typeof self != 'undefined' && self.Math == Math)
    ? self
    : Function('return this')()
;

$.fn.calendar = function(parameters) {
  var
    $allModules    = $(this),

    moduleSelector = $allModules.selector || '',

    time           = new Date().getTime(),
    performance    = [],

    query          = arguments[0],
    methodInvoked  = (typeof query == 'string'),
    queryArguments = [].slice.call(arguments, 1),
    returnedValue,
    timeGapTable = {
      '5': {'row': 4, 'column': 3 },
      '10': {'row': 3, 'column': 2 },
      '15': {'row': 2, 'column': 2 },
      '20': {'row': 3, 'column': 1 },
      '30': {'row': 2, 'column': 1 }
    },
    numberText = ['','one','two','three','four','five','six','seven','eight']
  ;

  $allModules
    .each(function () {
      var
        settings = ( $.isPlainObject(parameters) )
          ? $.extend(true, {}, $.fn.calendar.settings, parameters)
          : $.extend({}, $.fn.calendar.settings),

        className = settings.className,
        namespace = settings.namespace,
        selector = settings.selector,
        formatter = settings.formatter,
        parser = settings.parser,
        metadata = settings.metadata,
        timeGap = timeGapTable[settings.minTimeGap],
        error = settings.error,

        eventNamespace = '.' + namespace,
        moduleNamespace = 'module-' + namespace,

        $module = $(this),
        $input = $module.find(selector.input),
        $container = $module.find(selector.popup),
        $activator = $module.find(selector.activator),

        element = this,
        instance = $module.data(moduleNamespace),

        isTouch,
        isTouchDown = false,
        isInverted = $module.hasClass(className.inverted),
        focusDateUsedForRange = false,
        selectionComplete = false,
        classObserver,
        module
      ;

      module = {

        initialize: function () {
          module.debug('Initializing calendar for', element, $module);

          isTouch = module.get.isTouch();
          module.setup.config();
          module.setup.popup();
          module.setup.inline();
          module.setup.input();
          module.setup.date();
          module.create.calendar();

          module.bind.events();
          module.observeChanges();
          module.instantiate();
        },

        instantiate: function () {
          module.verbose('Storing instance of calendar');
          instance = module;
          $module.data(moduleNamespace, instance);
        },

        destroy: function () {
          module.verbose('Destroying previous calendar for', element);
          $module.removeData(moduleNamespace);
          module.unbind.events();
          module.disconnect.classObserver();
        },

        setup: {
          config: function () {
            if (module.get.minDate() !== null) {
              module.set.minDate($module.data(metadata.minDate));
            }
            if (module.get.maxDate() !== null) {
              module.set.maxDate($module.data(metadata.maxDate));
            }
            module.setting('type', module.get.type());
            module.setting('on', settings.on || ($input.length ? 'focus' : 'click'));
          },
          popup: function () {
            if (settings.inline) {
              return;
            }
            if (!$activator.length) {
              $activator = $module.children().first();
              if (!$activator.length) {
                return;
              }
            }
            if ($.fn.popup === undefined) {
              module.error(error.popup);
              return;
            }
            if (!$container.length) {
              //prepend the popup element to the activator's parent so that it has less chance of messing with
              //the styling (eg input action button needs to be the last child to have correct border radius)
              var $activatorParent = $activator.parent(),
                  domPositionFunction = $activatorParent.closest(selector.append).length !== 0 ? 'appendTo' : 'prependTo';
              $container = $('<div/>').addClass(className.popup)[domPositionFunction]($activatorParent);
            }
            $container.addClass(className.calendar);
            if(isInverted){
              $container.addClass(className.inverted);
            }
            var onVisible = function () {
              module.refreshTooltips();
              return settings.onVisible.apply($container, arguments);
            };
            var onHidden = settings.onHidden;
            if (!$input.length) {
              //no input, $container has to handle focus/blur
              $container.attr('tabindex', '0');
              onVisible = function () {
                module.refreshTooltips();
                module.focus();
                return settings.onVisible.apply($container, arguments);
              };
              onHidden = function () {
                module.blur();
                return settings.onHidden.apply($container, arguments);
              };
            }
            var onShow = function () {
              //reset the focus date onShow
              module.set.focusDate(module.get.date());
              module.set.mode(module.get.validatedMode(settings.startMode));
              return settings.onShow.apply($container, arguments);
            };
            var on = module.setting('on');
            var options = $.extend({}, settings.popupOptions, {
              popup: $container,
              on: on,
              hoverable: on === 'hover',
              closable: on === 'click',
              onShow: onShow,
              onVisible: onVisible,
              onHide: settings.onHide,
              onHidden: onHidden
            });
            module.popup(options);
          },
          inline: function () {
            if ($activator.length && !settings.inline) {
              return;
            }
            settings.inline = true;
            $container = $('<div/>').addClass(className.calendar).appendTo($module);
            if (!$input.length) {
              $container.attr('tabindex', '0');
            }
          },
          input: function () {
            if (settings.touchReadonly && $input.length && isTouch) {
              $input.prop('readonly', true);
            }
            module.check.disabled();
          },
          date: function () {
            var date;
            if (settings.initialDate) {
              date = parser.date(settings.initialDate, settings);
            } else if ($module.data(metadata.date) !== undefined) {
              date = parser.date($module.data(metadata.date), settings);
            } else if ($input.length) {
              date = parser.date($input.val(), settings);
            }
            module.set.date(date, settings.formatInput, false);
            module.set.mode(module.get.mode(), false);
          }
        },

        trigger: {
          change: function() {
            var
                inputElement = $input[0]
            ;
            if(inputElement) {
              var events = document.createEvent('HTMLEvents');
              module.verbose('Triggering native change event');
              events.initEvent('change', true, false);
              inputElement.dispatchEvent(events);
            }
          }
        },

        create: {
          calendar: function () {
            var i, r, c, p, row, cell, pageGrid;

            var
              mode = module.get.mode(),
              today = new Date(),
              date = module.get.date(),
              focusDate = module.get.focusDate(),
              display = module.helper.dateInRange(focusDate || date || settings.initialDate || today)
            ;

            if (!focusDate) {
              focusDate = display;
              module.set.focusDate(focusDate, false, false);
            }

            var
              isYear = mode === 'year',
              isMonth = mode === 'month',
              isDay = mode === 'day',
              isHour = mode === 'hour',
              isMinute = mode === 'minute',
              isTimeOnly = settings.type === 'time'
            ;

            var multiMonth = Math.max(settings.multiMonth, 1);
            var monthOffset = !isDay ? 0 : module.get.monthOffset();

            var
              minute = display.getMinutes(),
              hour = display.getHours(),
              day = display.getDate(),
              startMonth = display.getMonth() + monthOffset,
              year = display.getFullYear()
            ;

            var columns = isDay ? settings.showWeekNumbers ? 8 : 7 : isHour ? 4 : timeGap['column'];
            var rows = isDay || isHour ? 6 : timeGap['row'];
            var pages = isDay ? multiMonth : 1;

            var container = $container;
            var tooltipPosition = container.hasClass("left") ? "right center" : "left center";
            container.empty();
            if (pages > 1) {
              pageGrid = $('<div/>').addClass(className.grid).appendTo(container);
            }

            for (p = 0; p < pages; p++) {
              if (pages > 1) {
                var pageColumn = $('<div/>').addClass(className.column).appendTo(pageGrid);
                container = pageColumn;
              }

              var month = startMonth + p;
              var firstMonthDayColumn = (new Date(year, month, 1).getDay() - settings.firstDayOfWeek % 7 + 7) % 7;
              if (!settings.constantHeight && isDay) {
                var requiredCells = new Date(year, month + 1, 0).getDate() + firstMonthDayColumn;
                rows = Math.ceil(requiredCells / 7);
              }

              var
                yearChange = isYear ? 10 : isMonth ? 1 : 0,
                monthChange = isDay ? 1 : 0,
                dayChange = isHour || isMinute ? 1 : 0,
                prevNextDay = isHour || isMinute ? day : 1,
                prevDate = new Date(year - yearChange, month - monthChange, prevNextDay - dayChange, hour),
                nextDate = new Date(year + yearChange, month + monthChange, prevNextDay + dayChange, hour),
                prevLast = isYear ? new Date(Math.ceil(year / 10) * 10 - 9, 0, 0) :
                  isMonth ? new Date(year, 0, 0) : isDay ? new Date(year, month, 0) : new Date(year, month, day, -1),
                nextFirst = isYear ? new Date(Math.ceil(year / 10) * 10 + 1, 0, 1) :
                  isMonth ? new Date(year + 1, 0, 1) : isDay ? new Date(year, month + 1, 1) : new Date(year, month, day + 1)
              ;

              var tempMode = mode;
              if (isDay && settings.showWeekNumbers){
                tempMode += ' andweek';
              }
              var table = $('<table/>').addClass(className.table).addClass(tempMode).addClass(numberText[columns] + ' column').appendTo(container);
              if(isInverted){
                table.addClass(className.inverted);
              }
              var textColumns = columns;
              //no header for time-only mode
              if (!isTimeOnly) {
                var thead = $('<thead/>').appendTo(table);

                row = $('<tr/>').appendTo(thead);
                cell = $('<th/>').attr('colspan', '' + columns).appendTo(row);

                var headerDate = isYear || isMonth ? new Date(year, 0, 1) :
                  isDay ? new Date(year, month, 1) : new Date(year, month, day, hour, minute);
                var headerText = $('<span/>').addClass(className.link).appendTo(cell);
                headerText.text(formatter.header(headerDate, mode, settings));
                var newMode = isMonth ? (settings.disableYear ? 'day' : 'year') :
                  isDay ? (settings.disableMonth ? 'year' : 'month') : 'day';
                headerText.data(metadata.mode, newMode);

                if (p === 0) {
                  var prev = $('<span/>').addClass(className.prev).appendTo(cell);
                  prev.data(metadata.focusDate, prevDate);
                  prev.toggleClass(className.disabledCell, !module.helper.isDateInRange(prevLast, mode));
                  $('<i/>').addClass(className.prevIcon).appendTo(prev);
                }

                if (p === pages - 1) {
                  var next = $('<span/>').addClass(className.next).appendTo(cell);
                  next.data(metadata.focusDate, nextDate);
                  next.toggleClass(className.disabledCell, !module.helper.isDateInRange(nextFirst, mode));
                  $('<i/>').addClass(className.nextIcon).appendTo(next);
                }
                if (isDay) {
                  row = $('<tr/>').appendTo(thead);
                  if(settings.showWeekNumbers) {
                      cell = $('<th/>').appendTo(row);
                      cell.text(settings.text.weekNo);
                      cell.addClass(className.weekCell);
                      textColumns--;
                  }
                  for (i = 0; i < textColumns; i++) {
                    cell = $('<th/>').appendTo(row);
                    cell.text(formatter.dayColumnHeader((i + settings.firstDayOfWeek) % 7, settings));
                  }
                }
              }

              var tbody = $('<tbody/>').appendTo(table);
              i = isYear ? Math.ceil(year / 10) * 10 - 9 : isDay ? 1 - firstMonthDayColumn : 0;
              for (r = 0; r < rows; r++) {
                row = $('<tr/>').appendTo(tbody);
                if(isDay && settings.showWeekNumbers){
                    cell = $('<th/>').appendTo(row);
                    cell.text(module.get.weekOfYear(year,month,i+1-settings.firstDayOfWeek));
                    cell.addClass(className.weekCell);
                }
                for (c = 0; c < textColumns; c++, i++) {
                  var cellDate = isYear ? new Date(i, month, 1, hour, minute) :
                    isMonth ? new Date(year, i, 1, hour, minute) : isDay ? new Date(year, month, i, hour, minute) :
                      isHour ? new Date(year, month, day, i) : new Date(year, month, day, hour, i * settings.minTimeGap);
                  var cellText = isYear ? i :
                    isMonth ? settings.text.monthsShort[i] : isDay ? cellDate.getDate() :
                      formatter.time(cellDate, settings, true);
                  cell = $('<td/>').addClass(className.cell).appendTo(row);
                  cell.text(cellText);
                  cell.data(metadata.date, cellDate);
                  var adjacent = isDay && cellDate.getMonth() !== ((month + 12) % 12);
                  var disabled = (!settings.selectAdjacentDays && adjacent) || !module.helper.isDateInRange(cellDate, mode) || settings.isDisabled(cellDate, mode) || module.helper.isDisabled(cellDate, mode) || !module.helper.isEnabled(cellDate, mode);
                  if (disabled) {
                    var disabledDate = module.helper.findDayAsObject(cellDate, mode, settings.disabledDates);
                    if (disabledDate !== null && disabledDate[metadata.message]) {
                      cell.attr("data-tooltip", disabledDate[metadata.message]);
                      cell.attr("data-position", disabledDate[metadata.position] || tooltipPosition);
                      if(disabledDate[metadata.inverted] || (isInverted && disabledDate[metadata.inverted] === undefined)) {
                        cell.attr("data-inverted", '');
                      }
                      if(disabledDate[metadata.variation]) {
                        cell.attr("data-variation", disabledDate[metadata.variation]);
                      }
                    }
                  } else {
                    var eventDate = module.helper.findDayAsObject(cellDate, mode, settings.eventDates);
                    if (eventDate !== null) {
                      cell.addClass(eventDate[metadata.class] || settings.eventClass);
                      if (eventDate[metadata.message]) {
                        cell.attr("data-tooltip", eventDate[metadata.message]);
                        cell.attr("data-position", eventDate[metadata.position] || tooltipPosition);
                        if(eventDate[metadata.inverted] || (isInverted && eventDate[metadata.inverted] === undefined)) {
                          cell.attr("data-inverted", '');
                        }
                        if(eventDate[metadata.variation]) {
                          cell.attr("data-variation", eventDate[metadata.variation]);
                        }
                      }
                    }
                  }
                  var active = module.helper.dateEqual(cellDate, date, mode);
                  var isToday = module.helper.dateEqual(cellDate, today, mode);
                  cell.toggleClass(className.adjacentCell, adjacent);
                  cell.toggleClass(className.disabledCell, disabled);
                  cell.toggleClass(className.activeCell, active && !adjacent);
                  if (!isHour && !isMinute) {
                    cell.toggleClass(className.todayCell, !adjacent && isToday);
                  }

                  // Allow for external modifications of each cell
                  var cellOptions = {
                    mode: mode,
                    adjacent: adjacent,
                    disabled: disabled,
                    active: active,
                    today: isToday
                  };
                  formatter.cell(cell, cellDate, cellOptions);

                  if (module.helper.dateEqual(cellDate, focusDate, mode)) {
                    //ensure that the focus date is exactly equal to the cell date
                    //so that, if selected, the correct value is set
                    module.set.focusDate(cellDate, false, false);
                  }
                }
              }

              if (settings.today) {
                var todayRow = $('<tr/>').appendTo(tbody);
                var todayButton = $('<td/>').attr('colspan', '' + columns).addClass(className.today).appendTo(todayRow);
                todayButton.text(formatter.today(settings));
                todayButton.data(metadata.date, today);
              }

              module.update.focus(false, table);

              if(settings.inline){
                module.refreshTooltips();
              }
            }
          }
        },

        update: {
          focus: function (updateRange, container) {
            container = container || $container;
            var mode = module.get.mode();
            var date = module.get.date();
            var focusDate = module.get.focusDate();
            var startDate = module.get.startDate();
            var endDate = module.get.endDate();
            var rangeDate = (updateRange ? focusDate : null) || date || (!isTouch ? focusDate : null);

            container.find('td').each(function () {
              var cell = $(this);
              var cellDate = cell.data(metadata.date);
              if (!cellDate) {
                return;
              }
              var disabled = cell.hasClass(className.disabledCell);
              var active = cell.hasClass(className.activeCell);
              var adjacent = cell.hasClass(className.adjacentCell);
              var focused = module.helper.dateEqual(cellDate, focusDate, mode);
              var inRange = !rangeDate ? false :
                ((!!startDate && module.helper.isDateInRange(cellDate, mode, startDate, rangeDate)) ||
                (!!endDate && module.helper.isDateInRange(cellDate, mode, rangeDate, endDate)));
              cell.toggleClass(className.focusCell, focused && (!isTouch || isTouchDown) && (!adjacent || (settings.selectAdjacentDays && adjacent)) && !disabled);

              if (module.helper.isTodayButton(cell)) {
                return;
              }
              cell.toggleClass(className.rangeCell, inRange && !active && !disabled);
            });
          }
        },

        refresh: function () {
          module.create.calendar();
        },

        refreshTooltips: function() {
          var winWidth = $(window).width();
          $container.find('td[data-position]').each(function () {
            var cell = $(this);
            var tooltipWidth = window.getComputedStyle(cell[0], ':after').width.replace(/[^0-9\.]/g,'');
            var tooltipPosition = cell.attr('data-position');
            // use a fallback width of 250 (calendar width) for IE/Edge (which return "auto")
            var calcPosition = (winWidth - cell.width() - (parseInt(tooltipWidth,10) || 250)) > cell.offset().left ? 'right' : 'left';
            if(tooltipPosition.indexOf(calcPosition) === -1) {
              cell.attr('data-position',tooltipPosition.replace(/(left|right)/,calcPosition));
            }
           });
        },

        bind: {
          events: function () {
            module.debug('Binding events');
            $container.on('mousedown' + eventNamespace, module.event.mousedown);
            $container.on('touchstart' + eventNamespace, module.event.mousedown);
            $container.on('mouseup' + eventNamespace, module.event.mouseup);
            $container.on('touchend' + eventNamespace, module.event.mouseup);
            $container.on('mouseover' + eventNamespace, module.event.mouseover);
            if ($input.length) {
              $input.on('input' + eventNamespace, module.event.inputChange);
              $input.on('focus' + eventNamespace, module.event.inputFocus);
              $input.on('blur' + eventNamespace, module.event.inputBlur);
              $input.on('keydown' + eventNamespace, module.event.keydown);
            } else {
              $container.on('keydown' + eventNamespace, module.event.keydown);
            }
          }
        },

        unbind: {
          events: function () {
            module.debug('Unbinding events');
            $container.off(eventNamespace);
            if ($input.length) {
              $input.off(eventNamespace);
            }
          }
        },

        event: {
          mouseover: function (event) {
            var target = $(event.target);
            var date = target.data(metadata.date);
            var mousedown = event.buttons === 1;
            if (date) {
              module.set.focusDate(date, false, true, mousedown);
            }
          },
          mousedown: function (event) {
            if ($input.length) {
              //prevent the mousedown on the calendar causing the input to lose focus
              event.preventDefault();
            }
            isTouchDown = event.type.indexOf('touch') >= 0;
            var target = $(event.target);
            var date = target.data(metadata.date);
            if (date) {
              module.set.focusDate(date, false, true, true);
            }
          },
          mouseup: function (event) {
            //ensure input has focus so that it receives keydown events for calendar navigation
            module.focus();
            event.preventDefault();
            event.stopPropagation();
            isTouchDown = false;
            var target = $(event.target);
            if (target.hasClass("disabled")) {
              return;
            }
            var parent = target.parent();
            if (parent.data(metadata.date) || parent.data(metadata.focusDate) || parent.data(metadata.mode)) {
              //clicked on a child element, switch to parent (used when clicking directly on prev/next <i> icon element)
              target = parent;
            }
            var date = target.data(metadata.date);
            var focusDate = target.data(metadata.focusDate);
            var mode = target.data(metadata.mode);
            if (date && settings.onSelect.call(element, date, module.get.mode()) !== false) {
              var forceSet = target.hasClass(className.today);
              module.selectDate(date, forceSet);
            }
            else if (focusDate) {
              module.set.focusDate(focusDate);
            }
            else if (mode) {
              module.set.mode(mode);
            }
          },
          keydown: function (event) {
            var keyCode = event.which;
            if (keyCode === 27 || keyCode === 9) {
              //esc || tab
              module.popup('hide');
            }

            if (module.popup('is visible')) {
              if (keyCode === 37 || keyCode === 38 || keyCode === 39 || keyCode === 40) {
                //arrow keys
                var mode = module.get.mode();
                var bigIncrement = mode === 'day' ? 7 : mode === 'hour' ? 4 : mode === 'minute' ? timeGap['column'] : 3;
                var increment = keyCode === 37 ? -1 : keyCode === 38 ? -bigIncrement : keyCode == 39 ? 1 : bigIncrement;
                increment *= mode === 'minute' ? settings.minTimeGap : 1;
                var focusDate = module.get.focusDate() || module.get.date() || new Date();
                var year = focusDate.getFullYear() + (mode === 'year' ? increment : 0);
                var month = focusDate.getMonth() + (mode === 'month' ? increment : 0);
                var day = focusDate.getDate() + (mode === 'day' ? increment : 0);
                var hour = focusDate.getHours() + (mode === 'hour' ? increment : 0);
                var minute = focusDate.getMinutes() + (mode === 'minute' ? increment : 0);
                var newFocusDate = new Date(year, month, day, hour, minute);
                if (settings.type === 'time') {
                  newFocusDate = module.helper.mergeDateTime(focusDate, newFocusDate);
                }
                if (module.helper.isDateInRange(newFocusDate, mode)) {
                  module.set.focusDate(newFocusDate);
                }
              } else if (keyCode === 13) {
                //enter
                var mode = module.get.mode();
                var date = module.get.focusDate();
                if (date && !settings.isDisabled(date, mode) && !module.helper.isDisabled(date, mode) && module.helper.isEnabled(date, mode)) {
                  module.selectDate(date);
                }
                //disable form submission:
                event.preventDefault();
                event.stopPropagation();
              }
            }

            if (keyCode === 38 || keyCode === 40) {
              //arrow-up || arrow-down
              event.preventDefault(); //don't scroll
              module.popup('show');
            }
          },
          inputChange: function () {
            var val = $input.val();
            var date = parser.date(val, settings);
            module.set.date(date, false);
          },
          inputFocus: function () {
            $container.addClass(className.active);
          },
          inputBlur: function () {
            $container.removeClass(className.active);
            if (settings.formatInput) {
              var date = module.get.date();
              var text = formatter.datetime(date, settings);
              $input.val(text);
            }
            if(selectionComplete){
              module.trigger.change();
              selectionComplete = false;
            }
          },
          class: {
            mutation: function(mutations) {
              mutations.forEach(function(mutation) {
                if(mutation.attributeName === "class") {
                  module.check.disabled();
                }
              });
            }
          }
        },

        observeChanges: function() {
          if('MutationObserver' in window) {
            classObserver  = new MutationObserver(module.event.class.mutation);
            module.debug('Setting up mutation observer', classObserver);
            module.observe.class();
          }
        },

        disconnect: {
          classObserver: function() {
            if($input.length && classObserver) {
              classObserver.disconnect();
            }
          }
        },

        observe: {
          class: function() {
            if($input.length && classObserver) {
              classObserver.observe($module[0], {
                attributes : true
              });
            }
          }
        },

        is: {
          disabled: function() {
            return $module.hasClass(className.disabled);
          }
        },

        check: {
          disabled: function(){
            $input.attr('tabindex',module.is.disabled() ? -1 : 0);
          }
        },

        get: {
          weekOfYear: function(weekYear,weekMonth,weekDay) {
              // adapted from http://www.merlyn.demon.co.uk/weekcalc.htm
              var ms1d = 864e5, // milliseconds in a day
                  ms7d = 7 * ms1d; // milliseconds in a week

              return function() { // return a closure so constants get calculated only once
                  var DC3 = Date.UTC(weekYear, weekMonth, weekDay + 3) / ms1d, // an Absolute Day Number
                      AWN = Math.floor(DC3 / 7), // an Absolute Week Number
                      Wyr = new Date(AWN * ms7d).getUTCFullYear();

                  return AWN - Math.floor(Date.UTC(Wyr, 0, 7) / ms7d) + 1;
              }();
          },
          date: function () {
            return module.helper.sanitiseDate($module.data(metadata.date)) || null;
          },
          inputDate: function() {
            return $input.val();
          },
          focusDate: function () {
            return $module.data(metadata.focusDate) || null;
          },
          startDate: function () {
            var startModule = module.get.calendarModule(settings.startCalendar);
            return (startModule ? startModule.get.date() : $module.data(metadata.startDate)) || null;
          },
          endDate: function () {
            var endModule = module.get.calendarModule(settings.endCalendar);
            return (endModule ? endModule.get.date() : $module.data(metadata.endDate)) || null;
          },
          minDate: function() {
            return $module.data(metadata.minDate) || null;
          },
          maxDate: function() {
            return $module.data(metadata.maxDate) || null;
          },
          monthOffset: function () {
            return $module.data(metadata.monthOffset) || 0;
          },
          mode: function () {
            //only returns valid modes for the current settings
            var mode = $module.data(metadata.mode) || settings.startMode;
            return module.get.validatedMode(mode);
          },
          validatedMode: function(mode){
            var validModes = module.get.validModes();
            if ($.inArray(mode, validModes) >= 0) {
              return mode;
            }
            return settings.type === 'time' ? 'hour' :
              settings.type === 'month' ? 'month' :
                settings.type === 'year' ? 'year' : 'day';
          },
          type: function() {
            return $module.data(metadata.type) || settings.type;
          },
          validModes: function () {
            var validModes = [];
            if (settings.type !== 'time') {
              if (!settings.disableYear || settings.type === 'year') {
                validModes.push('year');
              }
              if (!(settings.disableMonth || settings.type === 'year') || settings.type === 'month') {
                validModes.push('month');
              }
              if (settings.type.indexOf('date') >= 0) {
                validModes.push('day');
              }
            }
            if (settings.type.indexOf('time') >= 0) {
              validModes.push('hour');
              if (!settings.disableMinute) {
                validModes.push('minute');
              }
            }
            return validModes;
          },
          isTouch: function () {
            try {
              document.createEvent('TouchEvent');
              return true;
            }
            catch (e) {
              return false;
            }
          },
          calendarModule: function (selector) {
            if (!selector) {
              return null;
            }
            if (!(selector instanceof $)) {
              selector = $(selector).first();
            }
            //assume range related calendars are using the same namespace
            return selector.data(moduleNamespace);
          }
        },

        set: {
          date: function (date, updateInput, fireChange) {
            updateInput = updateInput !== false;
            fireChange = fireChange !== false;
            date = module.helper.sanitiseDate(date);
            date = module.helper.dateInRange(date);

            var mode = module.get.mode();
            var text = formatter.datetime(date, settings);

            if (fireChange && settings.onBeforeChange.call(element, date, text, mode) === false) {
              return false;
            }

            module.set.focusDate(date);

            if (settings.isDisabled(date, mode)) {
              return false;
            }

            var endDate = module.get.endDate();
            if (!!endDate && !!date && date > endDate) {
              //selected date is greater than end date in range, so clear end date
              module.set.endDate(undefined);
            }
            module.set.dataKeyValue(metadata.date, date);

            if (updateInput && $input.length) {
              $input.val(text);
            }

            if (fireChange) {
              settings.onChange.call(element, date, text, mode);
            }
          },
          startDate: function (date, refreshCalendar) {
            date = module.helper.sanitiseDate(date);
            var startModule = module.get.calendarModule(settings.startCalendar);
            if (startModule) {
              startModule.set.date(date);
            }
            module.set.dataKeyValue(metadata.startDate, date, refreshCalendar);
          },
          endDate: function (date, refreshCalendar) {
            date = module.helper.sanitiseDate(date);
            var endModule = module.get.calendarModule(settings.endCalendar);
            if (endModule) {
              endModule.set.date(date);
            }
            module.set.dataKeyValue(metadata.endDate, date, refreshCalendar);
          },
          focusDate: function (date, refreshCalendar, updateFocus, updateRange) {
            date = module.helper.sanitiseDate(date);
            date = module.helper.dateInRange(date);
            var isDay = module.get.mode() === 'day';
            var oldFocusDate = module.get.focusDate();
            if (isDay && date && oldFocusDate) {
              var yearDelta = date.getFullYear() - oldFocusDate.getFullYear();
              var monthDelta = yearDelta * 12 + date.getMonth() - oldFocusDate.getMonth();
              if (monthDelta) {
                var monthOffset = module.get.monthOffset() - monthDelta;
                module.set.monthOffset(monthOffset, false);
              }
            }
            var changed = module.set.dataKeyValue(metadata.focusDate, date, !!date && refreshCalendar);
            updateFocus = (updateFocus !== false && changed && refreshCalendar === false) || focusDateUsedForRange != updateRange;
            focusDateUsedForRange = updateRange;
            if (updateFocus) {
              module.update.focus(updateRange);
            }
          },
          minDate: function (date) {
            date = module.helper.sanitiseDate(date);
            if (settings.maxDate !== null && settings.maxDate <= date) {
              module.verbose('Unable to set minDate variable bigger that maxDate variable', date, settings.maxDate);
            } else {
              module.setting('minDate', date);
              module.set.dataKeyValue(metadata.minDate, date);
            }
          },
          maxDate: function (date) {
            date = module.helper.sanitiseDate(date);
            if (settings.minDate !== null && settings.minDate >= date) {
              module.verbose('Unable to set maxDate variable lower that minDate variable', date, settings.minDate);
            } else {
              module.setting('maxDate', date);
              module.set.dataKeyValue(metadata.maxDate, date);
            }
          },
          monthOffset: function (monthOffset, refreshCalendar) {
            var multiMonth = Math.max(settings.multiMonth, 1);
            monthOffset = Math.max(1 - multiMonth, Math.min(0, monthOffset));
            module.set.dataKeyValue(metadata.monthOffset, monthOffset, refreshCalendar);
          },
          mode: function (mode, refreshCalendar) {
            module.set.dataKeyValue(metadata.mode, mode, refreshCalendar);
          },
          dataKeyValue: function (key, value, refreshCalendar) {
            var oldValue = $module.data(key);
            var equal = oldValue === value || (oldValue <= value && oldValue >= value); //equality test for dates and string objects
            if (value) {
              $module.data(key, value);
            } else {
              $module.removeData(key);
            }
            refreshCalendar = refreshCalendar !== false && !equal;
            if (refreshCalendar) {
              module.refresh();
            }
            return !equal;
          }
        },

        selectDate: function (date, forceSet) {
          module.verbose('New date selection', date);
          var mode = module.get.mode();
          var complete = forceSet || mode === 'minute' ||
            (settings.disableMinute && mode === 'hour') ||
            (settings.type === 'date' && mode === 'day') ||
            (settings.type === 'month' && mode === 'month') ||
            (settings.type === 'year' && mode === 'year');
          if (complete) {
            var canceled = module.set.date(date) === false;
            if (!canceled) {
              selectionComplete = true;
              if(settings.closable) {
                module.popup('hide');
                //if this is a range calendar, focus the container or input. This will open the popup from its event listeners.
                var endModule = module.get.calendarModule(settings.endCalendar);
                if (endModule) {
                  if (endModule.setting('on') !== 'focus') {
                    endModule.popup('show');
                  }
                  endModule.focus();
                }
              }
            }
          } else {
            var newMode = mode === 'year' ? (!settings.disableMonth ? 'month' : 'day') :
              mode === 'month' ? 'day' : mode === 'day' ? 'hour' : 'minute';
            module.set.mode(newMode);
            if (mode === 'hour' || (mode === 'day' && module.get.date())) {
              //the user has chosen enough to consider a valid date/time has been chosen
              module.set.date(date, true, false);
            } else {
              module.set.focusDate(date);
            }
          }
        },

        changeDate: function (date) {
          module.set.date(date);
        },

        clear: function () {
          module.set.date(undefined);
        },

        popup: function () {
          return $activator.popup.apply($activator, arguments);
        },

        focus: function () {
          if ($input.length) {
            $input.focus();
          } else {
            $container.focus();
          }
        },
        blur: function () {
          if ($input.length) {
            $input.blur();
          } else {
            $container.blur();
          }
        },

        helper: {
          isDisabled: function(date, mode) {
            return (mode === 'day' || mode === 'month' || mode === 'year') && ((settings.disabledDaysOfWeek.indexOf(date.getDay()) !== -1) || settings.disabledDates.some(function(d){
              if(typeof d === 'string') {
                d = module.helper.sanitiseDate(d);
              }
              if (d instanceof Date) {
                return module.helper.dateEqual(date, d, mode);
              }
              if (d !== null && typeof d === 'object') {
                if (d[metadata.year]) {
                  if (typeof d[metadata.year] === 'number') {
                    return date.getFullYear() == d[metadata.year];
                  } else if (Array.isArray(d[metadata.year])) {
                    return d[metadata.year].indexOf(date.getFullYear()) > -1;
                  }
                } else if (d[metadata.month]) {
                  if (typeof d[metadata.month] === 'number') {
                    return date.getMonth() == d[metadata.month];
                  } else if (Array.isArray(d[metadata.month])) {
                    return d[metadata.month].indexOf(date.getMonth()) > -1;
                  } else if (d[metadata.month] instanceof Date) {
                    var sdate = module.helper.sanitiseDate(d[metadata.month]);
                    return (date.getMonth() == sdate.getMonth()) && (date.getFullYear() == sdate.getFullYear())
                  }
                } else if (d[metadata.date] && mode === 'day') {
                  if (d[metadata.date] instanceof Date) {
                    return module.helper.dateEqual(date, module.helper.sanitiseDate(d[metadata.date]), mode);
                  } else if (Array.isArray(d[metadata.date])) {
                    return d[metadata.date].some(function(idate) {
                      return module.helper.dateEqual(date, idate, mode);
                    });
                  }
                }
              }
            }));
          },
          isEnabled: function(date, mode) {
            if (mode === 'day') {
              return settings.enabledDates.length === 0 || settings.enabledDates.some(function(d){
                if(typeof d === 'string') {
                  d = module.helper.sanitiseDate(d);
                }
                if (d instanceof Date) {
                  return module.helper.dateEqual(date, d, mode);
                }
                if (d !== null && typeof d === 'object' && d[metadata.date]) {
                  return module.helper.dateEqual(date, module.helper.sanitiseDate(d[metadata.date]), mode);
                }
              });
            } else {
              return true;
            }
          },
          findDayAsObject: function(date, mode, dates) {
            if (mode === 'day' || mode === 'month' || mode === 'year') {
              var d;
              for (var i = 0; i < dates.length; i++) {
                d = dates[i];
                if(typeof d === 'string') {
                  d = module.helper.sanitiseDate(d);
                }
                if (d instanceof Date && module.helper.dateEqual(date, d, mode)) {
                  var dateObject = {};
                  dateObject[metadata.date] = d;
                  return dateObject;
                }
                else if (d !== null && typeof d === 'object') {
                  if (d[metadata.year]) {
                    if (typeof d[metadata.year] === 'number' && date.getFullYear() == d[metadata.year]) {
                      return d;
                    } else if (Array.isArray(d[metadata.year])) {
                      if (d[metadata.year].indexOf(date.getFullYear()) > -1) {
                        return d;
                      }
                    }
                  } else if (d[metadata.month]) {
                    if (typeof d[metadata.month] === 'number' && date.getMonth() == d[metadata.month]) {
                      return d;
                    } else if (Array.isArray(d[metadata.month])) {
                      if (d[metadata.month].indexOf(date.getMonth()) > -1) {
                        return d;
                      }
                    } else if (d[metadata.month] instanceof Date) {
                      var sdate = module.helper.sanitiseDate(d[metadata.month]);
                      if ((date.getMonth() == sdate.getMonth()) && (date.getFullYear() == sdate.getFullYear())) {
                        return d;
                      }
                    }
                  } else if (d[metadata.date] && mode === 'day') {
                    if (d[metadata.date] instanceof Date && module.helper.dateEqual(date, module.helper.sanitiseDate(d[metadata.date]), mode)) {
                      return d;
                    } else if (Array.isArray(d[metadata.date])) {
                      if(d[metadata.date].some(function(idate) { return module.helper.dateEqual(date, idate, mode); })) {
                        return d;
                      }
                    }
                  }
                }
              }
            }
            return null;
          },
          sanitiseDate: function (date) {
            if (!date) {
              return undefined;
            }
            if (!(date instanceof Date)) {
              date = parser.date('' + date, settings);
            }
            if (!date || date === null || isNaN(date.getTime())) {
              return undefined;
            }
            return date;
          },
          dateDiff: function (date1, date2, mode) {
            mode = mode || 'day';
            var isTimeOnly = settings.type === 'time';
            var isYear = mode === 'year';
            var isYearOrMonth = isYear || mode === 'month';
            var isMinute = mode === 'minute';
            var isHourOrMinute = isMinute || mode === 'hour';
            //only care about a minute accuracy of settings.minTimeGap
            date1 = new Date(
              isTimeOnly ? 2000 : date1.getFullYear(),
              isTimeOnly ? 0 : isYear ? 0 : date1.getMonth(),
              isTimeOnly ? 1 : isYearOrMonth ? 1 : date1.getDate(),
              !isHourOrMinute ? 0 : date1.getHours(),
              !isMinute ? 0 : settings.minTimeGap * Math.floor(date1.getMinutes() / settings.minTimeGap));
            date2 = new Date(
              isTimeOnly ? 2000 : date2.getFullYear(),
              isTimeOnly ? 0 : isYear ? 0 : date2.getMonth(),
              isTimeOnly ? 1 : isYearOrMonth ? 1 : date2.getDate(),
              !isHourOrMinute ? 0 : date2.getHours(),
              !isMinute ? 0 : settings.minTimeGap * Math.floor(date2.getMinutes() / settings.minTimeGap));
            return date2.getTime() - date1.getTime();
          },
          dateEqual: function (date1, date2, mode) {
            return !!date1 && !!date2 && module.helper.dateDiff(date1, date2, mode) === 0;
          },
          isDateInRange: function (date, mode, minDate, maxDate) {
            if (!minDate && !maxDate) {
              var startDate = module.get.startDate();
              minDate = startDate && settings.minDate ? new Date(Math.max(startDate, settings.minDate)) : startDate || settings.minDate;
              maxDate = settings.maxDate;
            }
            minDate = minDate && new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate(), minDate.getHours(), settings.minTimeGap * Math.ceil(minDate.getMinutes() / settings.minTimeGap));
            return !(!date ||
            (minDate && module.helper.dateDiff(date, minDate, mode) > 0) ||
            (maxDate && module.helper.dateDiff(maxDate, date, mode) > 0));
          },
          dateInRange: function (date, minDate, maxDate) {
            if (!minDate && !maxDate) {
              var startDate = module.get.startDate();
              minDate = startDate && settings.minDate ? new Date(Math.max(startDate, settings.minDate)) : startDate || settings.minDate;
              maxDate = settings.maxDate;
            }
            minDate = minDate && new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate(), minDate.getHours(), settings.minTimeGap * Math.ceil(minDate.getMinutes() / settings.minTimeGap));
            var isTimeOnly = settings.type === 'time';
            return !date ? date :
              (minDate && module.helper.dateDiff(date, minDate, 'minute') > 0) ?
                (isTimeOnly ? module.helper.mergeDateTime(date, minDate) : minDate) :
                (maxDate && module.helper.dateDiff(maxDate, date, 'minute') > 0) ?
                  (isTimeOnly ? module.helper.mergeDateTime(date, maxDate) : maxDate) :
                  date;
          },
          mergeDateTime: function (date, time) {
            return (!date || !time) ? time :
              new Date(date.getFullYear(), date.getMonth(), date.getDate(), time.getHours(), time.getMinutes());
          },
          isTodayButton: function(element) {
            return element.text() === settings.text.today;
          }
        },

        setting: function (name, value) {
          module.debug('Changing setting', name, value);
          if ($.isPlainObject(name)) {
            $.extend(true, settings, name);
          }
          else if (value !== undefined) {
            if ($.isPlainObject(settings[name])) {
              $.extend(true, settings[name], value);
            }
            else {
              settings[name] = value;
            }
          }
          else {
            return settings[name];
          }
        },
        internal: function (name, value) {
          if( $.isPlainObject(name) ) {
            $.extend(true, module, name);
          }
          else if(value !== undefined) {
            module[name] = value;
          }
          else {
            return module[name];
          }
        },
        debug: function () {
          if (!settings.silent && settings.debug) {
            if (settings.performance) {
              module.performance.log(arguments);
            }
            else {
              module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
              module.debug.apply(console, arguments);
            }
          }
        },
        verbose: function () {
          if (!settings.silent && settings.verbose && settings.debug) {
            if (settings.performance) {
              module.performance.log(arguments);
            }
            else {
              module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
              module.verbose.apply(console, arguments);
            }
          }
        },
        error: function () {
          if (!settings.silent) {
            module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
            module.error.apply(console, arguments);
          }
        },
        performance: {
          log: function (message) {
            var
              currentTime,
              executionTime,
              previousTime
              ;
            if (settings.performance) {
              currentTime = new Date().getTime();
              previousTime = time || currentTime;
              executionTime = currentTime - previousTime;
              time = currentTime;
              performance.push({
                'Name': message[0],
                'Arguments': [].slice.call(message, 1) || '',
                'Element': element,
                'Execution Time': executionTime
              });
            }
            clearTimeout(module.performance.timer);
            module.performance.timer = setTimeout(module.performance.display, 500);
          },
          display: function () {
            var
              title = settings.name + ':',
              totalTime = 0
              ;
            time = false;
            clearTimeout(module.performance.timer);
            $.each(performance, function (index, data) {
              totalTime += data['Execution Time'];
            });
            title += ' ' + totalTime + 'ms';
            if (moduleSelector) {
              title += ' \'' + moduleSelector + '\'';
            }
            if ((console.group !== undefined || console.table !== undefined) && performance.length > 0) {
              console.groupCollapsed(title);
              if (console.table) {
                console.table(performance);
              }
              else {
                $.each(performance, function (index, data) {
                  console.log(data['Name'] + ': ' + data['Execution Time'] + 'ms');
                });
              }
              console.groupEnd();
            }
            performance = [];
          }
        },
        invoke: function (query, passedArguments, context) {
          var
            object = instance,
            maxDepth,
            found,
            response
            ;
          passedArguments = passedArguments || queryArguments;
          context = element || context;
          if (typeof query == 'string' && object !== undefined) {
            query = query.split(/[\. ]/);
            maxDepth = query.length - 1;
            $.each(query, function (depth, value) {
              var camelCaseValue = (depth != maxDepth)
                  ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
                  : query
                ;
              if ($.isPlainObject(object[camelCaseValue]) && (depth != maxDepth)) {
                object = object[camelCaseValue];
              }
              else if (object[camelCaseValue] !== undefined) {
                found = object[camelCaseValue];
                return false;
              }
              else if ($.isPlainObject(object[value]) && (depth != maxDepth)) {
                object = object[value];
              }
              else if (object[value] !== undefined) {
                found = object[value];
                return false;
              }
              else {
                module.error(error.method, query);
                return false;
              }
            });
          }
          if ($.isFunction(found)) {
            response = found.apply(context, passedArguments);
          }
          else if (found !== undefined) {
            response = found;
          }
          if (Array.isArray(returnedValue)) {
            returnedValue.push(response);
          }
          else if (returnedValue !== undefined) {
            returnedValue = [returnedValue, response];
          }
          else if (response !== undefined) {
            returnedValue = response;
          }
          return found;
        }
      };

      if (methodInvoked) {
        if (instance === undefined) {
          module.initialize();
        }
        module.invoke(query);
      }
      else {
        if (instance !== undefined) {
          instance.invoke('destroy');
        }
        module.initialize();
      }
    })
  ;
  return (returnedValue !== undefined)
    ? returnedValue
    : this
    ;
};

$.fn.calendar.settings = {

  name            : 'Calendar',
  namespace       : 'calendar',

  silent: false,
  debug: false,
  verbose: false,
  performance: false,

  type               : 'datetime', // picker type, can be 'datetime', 'date', 'time', 'month', or 'year'
  firstDayOfWeek     : 0,          // day for first day column (0 = Sunday)
  constantHeight     : true,       // add rows to shorter months to keep day calendar height consistent (6 rows)
  today              : false,      // show a 'today/now' button at the bottom of the calendar
  closable           : true,       // close the popup after selecting a date/time
  monthFirst         : true,       // month before day when parsing/converting date from/to text
  touchReadonly      : true,       // set input to readonly on touch devices
  inline             : false,      // create the calendar inline instead of inside a popup
  on                 : null,       // when to show the popup (defaults to 'focus' for input, 'click' for others)
  initialDate        : null,       // date to display initially when no date is selected (null = now)
  startMode          : false,      // display mode to start in, can be 'year', 'month', 'day', 'hour', 'minute' (false = 'day')
  minDate            : null,       // minimum date/time that can be selected, dates/times before are disabled
  maxDate            : null,       // maximum date/time that can be selected, dates/times after are disabled
  ampm               : true,       // show am/pm in time mode
  disableYear        : false,      // disable year selection mode
  disableMonth       : false,      // disable month selection mode
  disableMinute      : false,      // disable minute selection mode
  formatInput        : true,       // format the input text upon input blur and module creation
  startCalendar      : null,       // jquery object or selector for another calendar that represents the start date of a date range
  endCalendar        : null,       // jquery object or selector for another calendar that represents the end date of a date range
  multiMonth         : 1,          // show multiple months when in 'day' mode
  minTimeGap         : 5,
  showWeekNumbers    : null,       // show Number of Week at the very first column of a dayView
  disabledDates      : [],         // specific day(s) which won't be selectable and contain additional information.
  disabledDaysOfWeek : [],         // day(s) which won't be selectable(s) (0 = Sunday)
  enabledDates       : [],         // specific day(s) which will be selectable, all other days will be disabled
  eventDates         : [],         // specific day(s) which will be shown in a different color and using tooltips
  centuryBreak       : 60,         // starting short year until 99 where it will be assumed to belong to the last century
  currentCentury     : 2000,       // century to be added to 2-digit years (00 to {centuryBreak}-1)
  selectAdjacentDays : false,     // The calendar can show dates from adjacent month. These adjacent month dates can also be made selectable.
  // popup options ('popup', 'on', 'hoverable', and show/hide callbacks are overridden)
  popupOptions: {
    position: 'bottom left',
    lastResort: 'bottom left',
    prefer: 'opposite',
    hideOnScroll: false
  },

  text: {
    days: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
    months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
    monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
    today: 'Today',
    now: 'Now',
    am: 'AM',
    pm: 'PM',
    weekNo: 'Week'
  },

  formatter: {
    header: function (date, mode, settings) {
      return mode === 'year' ? settings.formatter.yearHeader(date, settings) :
        mode === 'month' ? settings.formatter.monthHeader(date, settings) :
          mode === 'day' ? settings.formatter.dayHeader(date, settings) :
            mode === 'hour' ? settings.formatter.hourHeader(date, settings) :
              settings.formatter.minuteHeader(date, settings);
    },
    yearHeader: function (date, settings) {
      var decadeYear = Math.ceil(date.getFullYear() / 10) * 10;
      return (decadeYear - 9) + ' - ' + (decadeYear + 2);
    },
    monthHeader: function (date, settings) {
      return date.getFullYear();
    },
    dayHeader: function (date, settings) {
      var month = settings.text.months[date.getMonth()];
      var year = date.getFullYear();
      return month + ' ' + year;
    },
    hourHeader: function (date, settings) {
      return settings.formatter.date(date, settings);
    },
    minuteHeader: function (date, settings) {
      return settings.formatter.date(date, settings);
    },
    dayColumnHeader: function (day, settings) {
      return settings.text.days[day];
    },
    datetime: function (date, settings) {
      if (!date) {
        return '';
      }
      var day = settings.type === 'time' ? '' : settings.formatter.date(date, settings);
      var time = settings.type.indexOf('time') < 0 ? '' : settings.formatter.time(date, settings, false);
      var separator = settings.type === 'datetime' ? ' ' : '';
      return day + separator + time;
    },
    date: function (date, settings) {
      if (!date) {
        return '';
      }
      var day = date.getDate();
      var month = settings.text.months[date.getMonth()];
      var year = date.getFullYear();
      return settings.type === 'year' ? year :
        settings.type === 'month' ? month + ' ' + year :
        (settings.monthFirst ? month + ' ' + day : day + ' ' + month) + ', ' + year;
    },
    time: function (date, settings, forCalendar) {
      if (!date) {
        return '';
      }
      var hour = date.getHours();
      var minute = date.getMinutes();
      var ampm = '';
      if (settings.ampm) {
        ampm = ' ' + (hour < 12 ? settings.text.am : settings.text.pm);
        hour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
      }
      return hour + ':' + (minute < 10 ? '0' : '') + minute + ampm;
    },
    today: function (settings) {
      return settings.type === 'date' ? settings.text.today : settings.text.now;
    },
    cell: function (cell, date, cellOptions) {
    }
  },

  parser: {
    date: function (text, settings) {
      if (text instanceof Date) {
        return text;
      }
      if (!text) {
        return null;
      }
      text = String(text).trim();
      if (text.length === 0) {
        return null;
      }
      if(text.match(/^[0-9]{4}[\/\-\.][0-9]{2}[\/\-\.][0-9]{2}$/)){
        text = text.replace(/[\/\-\.]/g,'/') + ' 00:00:00';
      }
      // Reverse date and month in some cases
      text = settings.monthFirst || !text.match(/^[0-9]{2}[\/\-\.]/) ? text : text.replace(/[\/\-\.]/g,'/').replace(/([0-9]+)\/([0-9]+)/,'$2/$1');
      var textDate = new Date(text);
      var numberOnly = text.match(/^[0-9]+$/) !== null;
      if(!numberOnly && !isNaN(textDate.getDate())) {
        return textDate;
      }
      text = text.toLowerCase();

      var i, j, k;
      var minute = -1, hour = -1, day = -1, month = -1, year = -1;
      var isAm = undefined;

      var isTimeOnly = settings.type === 'time';
      var isDateOnly = settings.type.indexOf('time') < 0;

      var words = text.split(settings.regExp.dateWords), word;
      var numbers = text.split(settings.regExp.dateNumbers), number;

      var parts;
      var monthString;

      if (!isDateOnly) {
        //am/pm
        isAm = $.inArray(settings.text.am.toLowerCase(), words) >= 0 ? true :
          $.inArray(settings.text.pm.toLowerCase(), words) >= 0 ? false : undefined;

        //time with ':'
        for (i = 0; i < numbers.length; i++) {
          number = numbers[i];
          if (number.indexOf(':') >= 0) {
            if (hour < 0 || minute < 0) {
              parts = number.split(':');
              for (k = 0; k < Math.min(2, parts.length); k++) {
                j = parseInt(parts[k]);
                if (isNaN(j)) {
                  j = 0;
                }
                if (k === 0) {
                  hour = j % 24;
                } else {
                  minute = j % 60;
                }
              }
            }
            numbers.splice(i, 1);
          }
        }
      }

      if (!isTimeOnly) {
        //textual month
        for (i = 0; i < words.length; i++) {
          word = words[i];
          if (word.length <= 0) {
            continue;
          }
          for (j = 0; j < settings.text.months.length; j++) {
            monthString = settings.text.months[j];
            monthString = monthString.substring(0, word.length).toLowerCase();
            if (monthString === word) {
              month = j + 1;
              break;
            }
          }
          if (month >= 0) {
            break;
          }
        }

        //year > settings.centuryBreak
        for (i = 0; i < numbers.length; i++) {
          j = parseInt(numbers[i]);
          if (isNaN(j)) {
            continue;
          }
          if (j >= settings.centuryBreak && i === numbers.length-1) {
            if (j <= 99) {
              j += settings.currentCentury - 100;
            }
            year = j;
            numbers.splice(i, 1);
            break;
          }
        }

        //numeric month
        if (month < 0) {
          for (i = 0; i < numbers.length; i++) {
            k = i > 1 || settings.monthFirst ? i : i === 1 ? 0 : 1;
            j = parseInt(numbers[k]);
            if (isNaN(j)) {
              continue;
            }
            if (1 <= j && j <= 12) {
              month = j;
              numbers.splice(k, 1);
              break;
            }
          }
        }

        //day
        for (i = 0; i < numbers.length; i++) {
          j = parseInt(numbers[i]);
          if (isNaN(j)) {
            continue;
          }
          if (1 <= j && j <= 31) {
            day = j;
            numbers.splice(i, 1);
            break;
          }
        }

        //year <= settings.centuryBreak
        if (year < 0) {
          for (i = numbers.length - 1; i >= 0; i--) {
            j = parseInt(numbers[i]);
            if (isNaN(j)) {
              continue;
            }
            if (j <= 99) {
              j += settings.currentCentury;
            }
            year = j;
            numbers.splice(i, 1);
            break;
          }
        }
      }

      if (!isDateOnly) {
        //hour
        if (hour < 0) {
          for (i = 0; i < numbers.length; i++) {
            j = parseInt(numbers[i]);
            if (isNaN(j)) {
              continue;
            }
            if (0 <= j && j <= 23) {
              hour = j;
              numbers.splice(i, 1);
              break;
            }
          }
        }

        //minute
        if (minute < 0) {
          for (i = 0; i < numbers.length; i++) {
            j = parseInt(numbers[i]);
            if (isNaN(j)) {
              continue;
            }
            if (0 <= j && j <= 59) {
              minute = j;
              numbers.splice(i, 1);
              break;
            }
          }
        }
      }

      if (minute < 0 && hour < 0 && day < 0 && month < 0 && year < 0) {
        return null;
      }

      if (minute < 0) {
        minute = 0;
      }
      if (hour < 0) {
        hour = 0;
      }
      if (day < 0) {
        day = 1;
      }
      if (month < 0) {
        month = 1;
      }
      if (year < 0) {
        year = new Date().getFullYear();
      }

      if (isAm !== undefined) {
        if (isAm) {
          if (hour === 12) {
            hour = 0;
          }
        } else if (hour < 12) {
          hour += 12;
        }
      }

      var date = new Date(year, month - 1, day, hour, minute);
      if (date.getMonth() !== month - 1 || date.getFullYear() !== year) {
        //month or year don't match up, switch to last day of the month
        date = new Date(year, month, 0, hour, minute);
      }
      return isNaN(date.getTime()) ? null : date;
    }
  },

  // callback before date is changed, return false to cancel the change
  onBeforeChange: function (date, text, mode) {
    return true;
  },

  // callback when date changes
  onChange: function (date, text, mode) {
  },

  // callback before show animation, return false to prevent show
  onShow: function () {
  },

  // callback after show animation
  onVisible: function () {
  },

  // callback before hide animation, return false to prevent hide
  onHide: function () {
  },

  // callback after hide animation
  onHidden: function () {
  },

  // callback before item is selected, return false to prevent selection
  onSelect: function (date, mode) {
  },

  // is the given date disabled?
  isDisabled: function (date, mode) {
    return false;
  },

  selector: {
    popup: '.ui.popup',
    input: 'input',
    activator: 'input',
    append: '.inline.field,.inline.fields'
  },

  regExp: {
    dateWords: /[^A-Za-z\u00C0-\u024F]+/g,
    dateNumbers: /[^\d:]+/g
  },

  error: {
    popup: 'UI Popup, a required component is not included in this page',
    method: 'The method you called is not defined.'
  },

  className: {
    calendar: 'calendar',
    active: 'active',
    popup: 'ui popup',
    grid: 'ui equal width grid',
    column: 'column',
    table: 'ui celled center aligned unstackable table',
    inverted: 'inverted',
    prev: 'prev link',
    next: 'next link',
    prevIcon: 'chevron left icon',
    nextIcon: 'chevron right icon',
    link: 'link',
    cell: 'link',
    disabledCell: 'disabled',
    weekCell: 'disabled',
    adjacentCell: 'adjacent',
    activeCell: 'active',
    rangeCell: 'range',
    focusCell: 'focus',
    todayCell: 'today',
    today: 'today link',
    disabled: 'disabled'
  },

  metadata: {
    date: 'date',
    focusDate: 'focusDate',
    startDate: 'startDate',
    endDate: 'endDate',
    minDate: 'minDate',
    maxDate: 'maxDate',
    mode: 'mode',
    type: 'type',
    monthOffset: 'monthOffset',
    message: 'message',
    class: 'class',
    inverted: 'inverted',
    variation: 'variation',
    position: 'position',
    month: 'month',
    year: 'year'
  },

  eventClass: 'blue'
};

})(jQuery, window, document);
